/* Riot WIP, @license MIT, (c) 2015 Muut Inc. + contributors */ ;(function(window, undefined) { 'use strict' var riot = { version: 'WIP', settings: {} } // This globals 'const' helps code size reduction // for typeof == '' comparisons var T_STRING = 'string', T_OBJECT = 'object', T_UNDEF = 'undefined' // for IE8 and rest of the world /* istanbul ignore next */ var isArray = Array.isArray || (function () { var _ts = Object.prototype.toString return function (v) { return _ts.call(v) === '[object Array]' } })() // Version# for IE 8-11, 0 for others var ieVersion = (function (win) { return (window && window.document || {}).documentMode | 0 })() riot.observable = function(el) { el = el || {} var callbacks = {}, _id = 0 el.on = function(events, fn) { if (isFunction(fn)) { if (typeof fn.id === T_UNDEF) fn._id = _id++ events.replace(/\S+/g, function(name, pos) { (callbacks[name] = callbacks[name] || []).push(fn) fn.typed = pos > 0 }) } return el } el.off = function(events, fn) { if (events == '*') callbacks = {} else { events.replace(/\S+/g, function(name) { if (fn) { var arr = callbacks[name] for (var i = 0, cb; (cb = arr && arr[i]); ++i) { if (cb._id == fn._id) arr.splice(i--, 1) } } else { callbacks[name] = [] } }) } return el } // only single event supported el.one = function(name, fn) { function on() { el.off(name, on) fn.apply(el, arguments) } return el.on(name, on) } el.trigger = function(name) { var args = [].slice.call(arguments, 1), fns = callbacks[name] || [] for (var i = 0, fn; (fn = fns[i]); ++i) { if (!fn.busy) { fn.busy = 1 fn.apply(el, fn.typed ? [name].concat(args) : args) if (fns[i] !== fn) { i-- } fn.busy = 0 } } if (callbacks.all && name != 'all') { el.trigger.apply(el, ['all', name].concat(args)) } return el } return el } riot.mixin = (function() { var mixins = {} return function(name, mixin) { if (!mixin) return mixins[name] mixins[name] = mixin } })() ;(function(riot, evt, win) { // browsers only if (!win) return var loc = win.location, fns = riot.observable(), started = false, current function hash() { return loc.href.split('#')[1] || '' } function parser(path) { return path.split('/') } function emit(path) { if (path.type) path = hash() if (path != current) { fns.trigger.apply(null, ['H'].concat(parser(path))) current = path } } var r = riot.route = function(arg) { // string if (arg[0]) { loc.hash = arg emit(arg) // function } else { fns.on('H', arg) } } r.exec = function(fn) { fn.apply(null, parser(hash())) } r.parser = function(fn) { parser = fn } r.stop = function () { if (!started) return win.removeEventListener ? win.removeEventListener(evt, emit, false) : win.detachEvent('on' + evt, emit) fns.off('*') started = false } r.start = function () { if (started) return win.addEventListener ? win.addEventListener(evt, emit, false) : win.attachEvent('on' + evt, emit) started = true } // autostart the router r.start() })(riot, 'hashchange', window) /* //// How it works? Three ways: 1. Expressions: tmpl('{ value }', data). Returns the result of evaluated expression as a raw object. 2. Templates: tmpl('Hi { name } { surname }', data). Returns a string with evaluated expressions. 3. Filters: tmpl('{ show: !done, highlight: active }', data). Returns a space separated list of trueish keys (mainly used for setting html classes), e.g. "show highlight". // Template examples tmpl('{ title || "Untitled" }', data) tmpl('Results are { results ? "ready" : "loading" }', data) tmpl('Today is { new Date() }', data) tmpl('{ message.length > 140 && "Message is too long" }', data) tmpl('This item got { Math.round(rating) } stars', data) tmpl('

{ title }

{ body }', data) // Falsy expressions in templates In templates (as opposed to single expressions) all falsy values except zero (undefined/null/false) will default to empty string: tmpl('{ undefined } - { false } - { null } - { 0 }', {}) // will return: " - - - 0" */ var brackets = (function(orig) { var cachedBrackets, r, b, re = /[{}]/g return function(x) { // make sure we use the current setting var s = riot.settings.brackets || orig // recreate cached vars if needed if (cachedBrackets !== s) { cachedBrackets = s b = s.split(' ') r = b.map(function (e) { return e.replace(/(?=.)/g, '\\') }) } // if regexp given, rewrite it with current brackets (only if differ from default) return x instanceof RegExp ? ( s === orig ? x : new RegExp(x.source.replace(re, function(b) { return r[~~(b === '}')] }), x.global ? 'g' : '') ) : // else, get specific bracket b[x] } })('{ }') var tmpl = (function() { var cache = {}, reVars = /(['"\/]).*?[^\\]\1|\.\w*|\w*:|\b(?:(?:new|typeof|in|instanceof) |(?:this|true|false|null|undefined)\b|function *\()|([a-z_$]\w*)/gi // [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ] // find variable names: // 1. skip quoted strings and regexps: "a b", 'a b', 'a \'b\'', /a b/ // 2. skip object properties: .name // 3. skip object literals: name: // 4. skip javascript keywords // 5. match var name // build a template (or get it from cache), render with data return function(str, data) { return str && (cache[str] = cache[str] || tmpl(str))(data) } // create a template instance function tmpl(s, p) { // default template string to {} s = (s || (brackets(0) + brackets(1))) // temporarily convert \{ and \} to a non-character .replace(brackets(/\\{/g), '\uFFF0') .replace(brackets(/\\}/g), '\uFFF1') // split string to expression and non-expresion parts p = split(s, extract(s, brackets(/{/), brackets(/}/))) return new Function('d', 'return ' + ( // is it a single expression or a template? i.e. {x} or {x} !p[0] && !p[2] && !p[3] // if expression, evaluate it ? expr(p[1]) // if template, evaluate all expressions in it : '[' + p.map(function(s, i) { // is it an expression or a string (every second part is an expression) return i % 2 // evaluate the expressions ? expr(s, true) // process string parts of the template: : '"' + s // preserve new lines .replace(/\n/g, '\\n') // escape quotes .replace(/"/g, '\\"') + '"' }).join(',') + '].join("")' ) // bring escaped { and } back .replace(/\uFFF0/g, brackets(0)) .replace(/\uFFF1/g, brackets(1)) + ';') } // parse { ... } expression function expr(s, n) { s = s // convert new lines to spaces .replace(/\n/g, ' ') // trim whitespace, brackets, strip comments .replace(brackets(/^[{ ]+|[ }]+$|\/\*.+?\*\//g), '') // is it an object literal? i.e. { key : value } return /^\s*[\w- "']+ *:/.test(s) // if object literal, return trueish keys // e.g.: { show: isOpen(), done: item.done } -> "show done" ? '[' + // extract key:val pairs, ignoring any nested objects extract(s, // name part: name:, "name":, 'name':, name : /["' ]*[\w- ]+["' ]*:/, // expression part: everything upto a comma followed by a name (see above) or end of line /,(?=["' ]*[\w- ]+["' ]*:)|}|$/ ).map(function(pair) { // get key, val parts return pair.replace(/^[ "']*(.+?)[ "']*: *(.+?),? *$/, function(_, k, v) { // wrap all conditional parts to ignore errors return v.replace(/[^&|=!><]+/g, wrap) + '?"' + k + '":"",' }) }).join('') + '].join(" ").trim()' // if js expression, evaluate as javascript : wrap(s, n) } // execute js w/o breaking on errors or undefined vars function wrap(s, nonull) { s = s.trim() return !s ? '' : '(function(v){try{v=' // prefix vars (name => data.name) + (s.replace(reVars, function(s, _, v) { return v ? '(d.'+v+'===undefined?'+(typeof window == 'undefined' ? 'global.' : 'window.')+v+':d.'+v+')' : s }) // break the expression if its empty (resulting in undefined value) || 'x') + '}catch(e){' + '}finally{return ' // default to empty string for falsy values except zero + (nonull === true ? '!v&&v!==0?"":v' : 'v') + '}}).call(d)' } // split string by an array of substrings function split(str, substrings) { var parts = [] substrings.map(function(sub, i) { // push matched expression and part before it i = str.indexOf(sub) parts.push(str.slice(0, i), sub) str = str.slice(i + sub.length) }) // push the remaining part return parts.concat(str) } // match strings between opening and closing regexp, skipping any inner/nested matches function extract(str, open, close) { var start, level = 0, matches = [], re = new RegExp('('+open.source+')|('+close.source+')', 'g') str.replace(re, function(_, open, close, pos) { // if outer inner bracket, mark position if (!level && open) start = pos // in(de)crease bracket level level += open ? 1 : -1 // if outer closing bracket, grab the match if (!level && close != null) matches.push(str.slice(start, pos+close.length)) }) return matches } })() // { key, i in items} -> { key, i, items } function loopKeys(expr) { var b0 = brackets(0), els = expr.trim().slice(b0.length).match(/^\s*(\S+?)\s*(?:,\s*(\S+))?\s+in\s+(.+)$/) return els ? { key: els[1], pos: els[2], val: b0 + els[3] } : { val: expr } } function mkitem(expr, key, val) { var item = {} item[expr.key] = key if (expr.pos) item[expr.pos] = val return item } /* Beware: heavy stuff */ function _each(dom, parent, expr) { remAttr(dom, 'each') var tagName = getTagName(dom), template = dom.outerHTML, hasImpl = !!tagImpl[tagName], impl = tagImpl[tagName] || { tmpl: template }, root = dom.parentNode, placeholder = document.createComment('riot placeholder'), tags = [], child = getTag(dom), checksum root.insertBefore(placeholder, dom) expr = loopKeys(expr) // clean template code parent .one('premount', function () { if (root.stub) root = parent.root // remove the original DOM node dom.parentNode.removeChild(dom) }) .on('update', function () { var items = tmpl(expr.val, parent) // object loop. any changes cause full redraw if (!isArray(items)) { checksum = items ? JSON.stringify(items) : '' items = !items ? [] : Object.keys(items).map(function (key) { return mkitem(expr, key, items[key]) }) } var frag = document.createDocumentFragment(), i = tags.length, j = items.length // unmount leftover items while (i > j) { tags[--i].unmount() tags.splice(i, 1) } for (i = 0; i < j; ++i) { var _item = !checksum && !!expr.key ? mkitem(expr, items[i], i) : items[i] if (!tags[i]) { // mount new (tags[i] = new Tag(impl, { parent: parent, isLoop: true, hasImpl: hasImpl, root: hasImpl ? dom.cloneNode() : root, item: _item }, dom.innerHTML) ).mount() frag.appendChild(tags[i].root) } else tags[i].update(_item) tags[i]._item = _item } root.insertBefore(frag, placeholder) if (child) parent.tags[tagName] = tags }).one('updated', function() { var keys = Object.keys(parent)// only set new values walk(root, function(node) { // only set element node and not isLoop if (node.nodeType == 1 && !node.isLoop && !node._looped) { node._visited = false // reset _visited for loop node node._looped = true // avoid set multiple each setNamed(node, parent, keys) } }) }) } function parseNamedElements(root, parent, childTags) { walk(root, function(dom) { if (dom.nodeType == 1) { dom.isLoop = dom.isLoop || (dom.parentNode && dom.parentNode.isLoop || dom.getAttribute('each')) ? 1 : 0 // custom child tag var child = getTag(dom) if (child && !dom.isLoop) { var tag = new Tag(child, { root: dom, parent: parent }, dom.innerHTML), tagName = getTagName(dom), ptag = parent, cachedTag while (!getTag(ptag.root)) { if (!ptag.parent) break ptag = ptag.parent } // fix for the parent attribute in the looped elements tag.parent = ptag cachedTag = ptag.tags[tagName] // if there are multiple children tags having the same name if (cachedTag) { // if the parent tags property is not yet an array // create it adding the first cached tag if (!isArray(cachedTag)) ptag.tags[tagName] = [cachedTag] // add the new nested tag to the array ptag.tags[tagName].push(tag) } else { ptag.tags[tagName] = tag } // empty the child node once we got its template // to avoid that its children get compiled multiple times dom.innerHTML = '' childTags.push(tag) } if (!dom.isLoop) setNamed(dom, parent, []) } }) } function parseExpressions(root, tag, expressions) { function addExpr(dom, val, extra) { if (val.indexOf(brackets(0)) >= 0) { var expr = { dom: dom, expr: val } expressions.push(extend(expr, extra)) } } walk(root, function(dom) { var type = dom.nodeType // text node if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue) if (type != 1) return /* element */ // loop var attr = dom.getAttribute('each') if (attr) { _each(dom, tag, attr); return false } // attribute expressions each(dom.attributes, function(attr) { var name = attr.name, bool = name.split('__')[1] addExpr(dom, attr.value, { attr: bool || name, bool: bool }) if (bool) { remAttr(dom, name); return false } }) // skip custom tags if (getTag(dom)) return false }) } function Tag(impl, conf, innerHTML) { var self = riot.observable(this), opts = inherit(conf.opts) || {}, dom = mkdom(impl.tmpl), parent = conf.parent, isLoop = conf.isLoop, hasImpl = conf.hasImpl, item = cleanUpData(conf.item), expressions = [], childTags = [], root = conf.root, fn = impl.fn, tagName = root.tagName.toLowerCase(), attr = {}, propsInSyncWithParent = [], loopDom, TAG_ATTRIBUTES = /([\w\-]+)\s?=\s?['"]([^'"]+)["']/gim if (fn && root._tag) { root._tag.unmount(true) } // not yet mounted this.isMounted = false root.isLoop = isLoop if (impl.attrs) { var attrs = impl.attrs.match(TAG_ATTRIBUTES) each(attrs, function(a) { var kv = a.split(/\s?=\s?/) root.setAttribute(kv[0], kv[1].replace(/['"]/g, '')) }) } // keep a reference to the tag just created // so we will be able to mount this tag multiple times root._tag = this // create a unique id to this tag // it could be handy to use it also to improve the virtual dom rendering speed this._id = fastAbs(~~(Date.now() * Math.random())) extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item) // grab attributes each(root.attributes, function(el) { var val = el.value // remember attributes with expressions only if (brackets(/\{.*\}/).test(val)) attr[el.name] = val }) if (dom.innerHTML && !/select|optgroup|tbody|tr/.test(tagName)) // replace all the yield tags with the tag inner html dom.innerHTML = replaceYield(dom.innerHTML, innerHTML) // options function updateOpts() { var ctx = hasImpl && isLoop ? self : parent || self // update opts from current DOM attributes each(root.attributes, function(el) { opts[el.name] = tmpl(el.value, ctx) }) // recover those with expressions each(Object.keys(attr), function(name) { opts[name] = tmpl(attr[name], ctx) }) } function normalizeData(data) { for (var key in item) { if (typeof self[key] !== T_UNDEF) self[key] = data[key] } } function inheritFromParent () { if (!self.parent || !isLoop) return each(Object.keys(self.parent), function(k) { // some properties must be always in sync with the parent tag var mustSync = ~propsInSyncWithParent.indexOf(k) if (typeof self[k] === T_UNDEF || mustSync) { // track the property to keep in sync // so we can keep it updated if (!mustSync) propsInSyncWithParent.push(k) self[k] = self.parent[k] } }) } this.update = function(data) { // make sure the data passed will not override // the component core methods data = cleanUpData(data) // inherit properties from the parent inheritFromParent() // normalize the tag properties in case an item object was initially passed if (data && typeof item === T_OBJECT || isArray(item)) { normalizeData(data) item = data } extend(self, data) updateOpts() self.trigger('update', data) update(expressions, self) self.trigger('updated') } this.mixin = function() { each(arguments, function(mix) { mix = typeof mix === T_STRING ? riot.mixin(mix) : mix each(Object.keys(mix), function(key) { // bind methods to self if (key != 'init') self[key] = isFunction(mix[key]) ? mix[key].bind(self) : mix[key] }) // init method will be called automatically if (mix.init) mix.init.bind(self)() }) } this.mount = function() { updateOpts() // initialiation fn && fn.call(self, opts) toggle(true) // parse layout after init. fn may calculate args for nested custom tags parseExpressions(dom, self, expressions) if (!self.parent || hasImpl) parseExpressions(self.root, self, expressions) // top level before update, empty root if (!self.parent || isLoop) self.update(item) // internal use only, fixes #403 self.trigger('premount') if (isLoop && !hasImpl) { // update the root attribute for the looped elements self.root = root = loopDom = dom.firstChild } else { while (dom.firstChild) root.appendChild(dom.firstChild) if (root.stub) self.root = root = parent.root } // if it's not a child tag we can trigger its mount event if (!self.parent || self.parent.isMounted) { self.isMounted = true self.trigger('mount') } // otherwise we need to wait that the parent event gets triggered else self.parent.one('mount', function() { // avoid to trigger the `mount` event for the tags // not visible included in an if statement if (!isInStub(self.root)) { self.parent.isMounted = self.isMounted = true self.trigger('mount') } }) } this.unmount = function(keepRootTag) { var el = loopDom || root, p = el.parentNode if (p) { if (parent) // remove this tag from the parent tags object // if there are multiple nested tags with same name.. // remove this element form the array if (isArray(parent.tags[tagName])) each(parent.tags[tagName], function(tag, i) { if (tag._id == self._id) parent.tags[tagName].splice(i, 1) }) else // otherwise just delete the tag instance parent.tags[tagName] = undefined else while (el.firstChild) el.removeChild(el.firstChild) if (!keepRootTag) p.removeChild(el) } self.trigger('unmount') toggle() self.off('*') // somehow ie8 does not like `delete root._tag` root._tag = null } function toggle(isMount) { // mount/unmount children each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() }) // listen/unlisten parent (events flow one way from parent to children) if (parent) { var evt = isMount ? 'on' : 'off' // the loop tags will be always in sync with the parent automatically if (isLoop) parent[evt]('unmount', self.unmount) else parent[evt]('update', self.update)[evt]('unmount', self.unmount) } } // named elements available for fn parseNamedElements(dom, this, childTags) } function setEventHandler(name, handler, dom, tag) { dom[name] = function(e) { var item = tag._item, ptag = tag.parent if (!item) while (ptag) { item = ptag._item ptag = item ? false : ptag.parent } // cross browser event fix e = e || window.event // ignore error on some browsers try { e.currentTarget = dom if (!e.target) e.target = e.srcElement if (!e.which) e.which = e.charCode || e.keyCode } catch (ignored) { '' } e.item = item // prevent default behaviour (by default) if (handler.call(tag, e) !== true && !/radio|check/.test(dom.type)) { e.preventDefault && e.preventDefault() e.returnValue = false } if (!e.preventUpdate) { var el = item ? tag.parent : tag el.update() } } } // used by if- attribute function insertTo(root, node, before) { if (root) { root.insertBefore(before, node) root.removeChild(node) } } function update(expressions, tag) { each(expressions, function(expr, i) { var dom = expr.dom, attrName = expr.attr, value = tmpl(expr.expr, tag), parent = expr.dom.parentNode if (value == null) value = '' // leave out riot- prefixes from strings inside textarea if (parent && parent.tagName == 'TEXTAREA') value = value.replace(/riot-/g, '') // no change if (expr.value === value) return expr.value = value // text node if (!attrName) return dom.nodeValue = value.toString() // remove original attribute remAttr(dom, attrName) // event handler if (isFunction(value)) { setEventHandler(attrName, value, dom, tag) // if- conditional } else if (attrName == 'if') { var stub = expr.stub // add to DOM if (value) { if (stub) { insertTo(stub.parentNode, stub, dom) dom.inStub = false // avoid to trigger the mount event if the tags is not visible yet // maybe we can optimize this avoiding to mount the tag at all if (!isInStub(dom)) { walk(dom, function(el) { if (el._tag && !el._tag.isMounted) el._tag.isMounted = !!el._tag.trigger('mount') }) } } // remove from DOM } else { stub = expr.stub = stub || document.createTextNode('') insertTo(dom.parentNode, dom, stub) dom.inStub = true } // show / hide } else if (/^(show|hide)$/.test(attrName)) { if (attrName == 'hide') value = !value dom.style.display = value ? '' : 'none' // field value } else if (attrName == 'value') { dom.value = value // } else if (attrName.slice(0, 5) == 'riot-' && attrName != 'riot-tag') { attrName = attrName.slice(5) value ? dom.setAttribute(attrName, value) : remAttr(dom, attrName) } else { if (expr.bool) { dom[attrName] = value if (!value) return value = attrName } if (typeof value !== T_OBJECT) dom.setAttribute(attrName, value) } }) } function each(els, fn) { for (var i = 0, len = (els || []).length, el; i < len; i++) { el = els[i] // return false -> remove current item during loop if (el != null && fn(el, i) === false) i-- } return els } function isFunction(v) { return typeof v === 'function' || false // avoid IE problems } function remAttr(dom, name) { dom.removeAttribute(name) } function fastAbs(nr) { return (nr ^ (nr >> 31)) - (nr >> 31) } function getTag(dom) { var tagName = dom.tagName.toLowerCase() return tagImpl[dom.getAttribute(RIOT_TAG) || tagName] } function getTagName(dom) { var child = getTag(dom), namedTag = dom.getAttribute('name'), tagName = namedTag && namedTag.indexOf(brackets(0)) < 0 ? namedTag : child ? child.name : dom.tagName.toLowerCase() return tagName } function extend(src) { var obj, args = arguments for (var i = 1; i < args.length; ++i) { if ((obj = args[i])) { for (var key in obj) { // eslint-disable-line guard-for-in src[key] = obj[key] } } } return src } // with this function we avoid that the current Tag methods get overridden function cleanUpData(data) { if (!(data instanceof Tag)) return data var o = {}, blackList = ['update', 'root', 'mount', 'unmount', 'mixin', 'isMounted', 'isloop', 'tags', 'parent', 'opts'] for (var key in data) { if (!~blackList.indexOf(key)) o[key] = data[key] } return o } function mkdom(template) { var checkie = ieVersion && ieVersion < 10, matches = /^\s*<([\w-]+)/.exec(template), tagName = matches ? matches[1].toLowerCase() : '', rootTag = (tagName === 'th' || tagName === 'td') ? 'tr' : (tagName === 'tr' ? 'tbody' : 'div'), el = mkEl(rootTag) el.stub = true if (checkie) { if (tagName === 'optgroup') optgroupInnerHTML(el, template) else if (tagName === 'option') optionInnerHTML(el, template) else if (rootTag !== 'div') tbodyInnerHTML(el, template, tagName) else checkie = 0 } if (!checkie) el.innerHTML = template return el } function walk(dom, fn) { if (dom) { if (fn(dom) === false) walk(dom.nextSibling, fn) else { dom = dom.firstChild while (dom) { walk(dom, fn) dom = dom.nextSibling } } } } function isInStub(dom) { while (dom) { if (dom.inStub) return true dom = dom.parentNode } return false } function mkEl(name) { return document.createElement(name) } function replaceYield (tmpl, innerHTML) { return tmpl.replace(/<(yield)\/?>(<\/\1>)?/gim, innerHTML || '') } function $$(selector, ctx) { return (ctx || document).querySelectorAll(selector) } function $(selector, ctx) { return (ctx || document).querySelector(selector) } function inherit(parent) { function Child() {} Child.prototype = parent return new Child() } function setNamed(dom, parent, keys) { each(dom.attributes, function(attr) { if (dom._visited) return if (attr.name === 'id' || attr.name === 'name') { dom._visited = true var p, v = attr.value if (~keys.indexOf(v)) return p = parent[v] if (!p) parent[v] = dom else isArray(p) ? p.push(dom) : (parent[v] = [p, dom]) } }) } /** * * Hacks needed for the old internet explorer versions [lower than IE10] * */ /* istanbul ignore next */ function tbodyInnerHTML(el, html, tagName) { var div = mkEl('div'), loops = /td|th/.test(tagName) ? 3 : 2, child div.innerHTML = '' + html + '
' child = div.firstChild while (loops--) child = child.firstChild el.appendChild(child) } /* istanbul ignore next */ function optionInnerHTML(el, html) { var opt = mkEl('option'), valRegx = /value=[\"'](.+?)[\"']/, selRegx = /selected=[\"'](.+?)[\"']/, eachRegx = /each=[\"'](.+?)[\"']/, ifRegx = /if=[\"'](.+?)[\"']/, innerRegx = />([^<]*)]*)>/, tagRegx = /^<([^ \>]*)/, labelMatch = html.match(labelRegx), elementMatch = html.match(elementRegx), tagMatch = html.match(tagRegx), innerContent = html if (elementMatch) { var options = html.slice(elementMatch[1].length+2, -tagMatch[1].length-3).trim() innerContent = options } if (labelMatch) opt.setAttribute('riot-label', labelMatch[1]) if (innerContent) { var innerOpt = mkEl('div') optionInnerHTML(innerOpt, innerContent) opt.appendChild(innerOpt.firstChild) } el.appendChild(opt) } /* Virtual dom is an array of custom tags on the document. Updates and unmounts propagate downwards from parent to children. */ var virtualDom = [], tagImpl = {}, styleNode var RIOT_TAG = 'riot-tag' function injectStyle(css) { if (riot.render) return // skip injection on the server if (!styleNode) { styleNode = mkEl('style') styleNode.setAttribute('type', 'text/css') } var head = document.head || document.getElementsByTagName('head')[0] if (styleNode.styleSheet) styleNode.styleSheet.cssText += css else styleNode.innerHTML += css if (!styleNode._rendered) if (styleNode.styleSheet) { document.body.appendChild(styleNode) } else { var rs = $('style[type=riot]') if (rs) { rs.parentNode.insertBefore(styleNode, rs) rs.parentNode.removeChild(rs) } else head.appendChild(styleNode) } styleNode._rendered = true } function mountTo(root, tagName, opts) { var tag = tagImpl[tagName], // cache the inner HTML to fix #855 innerHTML = root._innerHTML = root._innerHTML || root.innerHTML // clear the inner html root.innerHTML = '' if (tag && root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML) if (tag && tag.mount) { tag.mount() virtualDom.push(tag) return tag.on('unmount', function() { virtualDom.splice(virtualDom.indexOf(tag), 1) }) } } riot.tag = function(name, html, css, attrs, fn) { if (isFunction(attrs)) { fn = attrs if (/^[\w\-]+\s?=/.test(css)) { attrs = css css = '' } else attrs = '' } if (css) { if (isFunction(css)) fn = css else injectStyle(css) } tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } return name } riot.mount = function(selector, tagName, opts) { var els, allTags, tags = [] // helper functions function addRiotTags(arr) { var list = '' each(arr, function (e) { list += ', *[riot-tag="'+ e.trim() + '"]' }) return list } function selectAllTags() { var keys = Object.keys(tagImpl) return keys + addRiotTags(keys) } function pushTags(root) { var last if (root.tagName) { if (tagName && (!(last = root.getAttribute(RIOT_TAG)) || last != tagName)) root.setAttribute(RIOT_TAG, tagName) var tag = mountTo(root, tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts) if (tag) tags.push(tag) } else if (root.length) { each(root, pushTags) // assume nodeList } } // ----- mount code ----- if (typeof tagName === T_OBJECT) { opts = tagName tagName = 0 } // crawl the DOM to find the tag if (typeof selector === T_STRING) { if (selector === '*') // select all the tags registered // and also the tags found with the riot-tag attribute set selector = allTags = selectAllTags() else // or just the ones named like the selector selector += addRiotTags(selector.split(',')) els = $$(selector) } else // probably you have passed already a tag or a NodeList els = selector // select all the registered and mount them inside their root elements if (tagName === '*') { // get all custom tags tagName = allTags || selectAllTags() // if the root els it's just a single tag if (els.tagName) els = $$(tagName, els) else { // select all the children for all the different root elements var nodeList = [] each(els, function (_el) { nodeList.push($$(tagName, _el)) }) els = nodeList } // get rid of the tagName tagName = 0 } if (els.tagName) pushTags(els) else each(els, pushTags) return tags } // update everything riot.update = function() { return each(virtualDom, function(tag) { tag.update() }) } // @deprecated riot.mountTo = riot.mount var parsers = { html: {}, css: {}, js: { coffee: function(js) { return CoffeeScript.compile(js, { bare: true }) }, es6: function(js) { return babel.transform(js, { blacklist: ['useStrict'] }).code }, none: function(js) { return js } } } // fix 913 parsers.js.javascript = parsers.js.none // 4 the nostalgics parsers.js.coffeescript = parsers.js.coffee riot.parsers = parsers var BOOL_ATTR = ('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,'+ 'defaultchecked,defaultmuted,defaultselected,defer,disabled,draggable,enabled,formnovalidate,hidden,'+ 'indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,'+ 'pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,spellcheck,translate,truespeed,'+ 'typemustmatch,visible').split(','), // these cannot be auto-closed VOID_TAGS = 'area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr'.split(','), /* Following attributes give error when parsed on browser with { exrp_values } 'd' describes the SVG , Chrome gives error if the value is not valid format https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d */ PREFIX_ATTR = ['style', 'src', 'd'], LINE_TAG = /^<([\w\-]+)>(.*)<\/\1>/gim, QUOTE = /=({[^}]+})([\s\/\>])/g, SET_ATTR = /([\w\-]+)=(["'])([^\2]+?)\2/g, EXPR = /{\s*([^}]+)\s*}/g, // (tagname) (html) (javascript) endtag CUSTOM_TAG = /^<([\w\-]+)\s?([^>]*)>([^\x00]*[\w\/}"']>$)?([^\x00]*?)^<\/\1>/gim, SCRIPT = /'"]+)['"]?)?>([^\x00]*?)<\/script>/gm, STYLE = /'"]+)['"]?|\s+scoped)?>([^\x00]*?)<\/style>/gm, CSS_SELECTOR = /(^|\}|\{)\s*([^\{\}]+)\s*(?=\{)/g, CSS_COMMENT = /\/\*[^\x00]*?\*\//gm, HTML_COMMENT = //g, CLOSED_TAG = /<([\w\-]+)([^>]*)\/\s*>/g, LINE_COMMENT = /^\s*\/\/.*$/gm, JS_COMMENT = /\/\*[^\x00]*?\*\//gm, INPUT_NUMBER = /(]*?)type=['"]number['"]/gm function mktag(name, html, css, attrs, js) { return 'riot.tag(\'' + name + '\', \'' + html + '\'' + (css ? ', \'' + css + '\'' : '') + (attrs ? ', \'' + attrs.replace(/'/g, "\\'") + '\'' : '') + ', function(opts) {' + js + '\n});' } function compileHTML(html, opts, type) { var brackets = riot.util.brackets // foo={ bar } --> foo="{ bar }" html = html.replace(brackets(QUOTE), '="$1"$2') // whitespace html = opts.whitespace ? html.replace(/\n/g, '\\n') : html.replace(/\s+/g, ' ') // strip comments html = html.trim().replace(HTML_COMMENT, '') // input type=numbr html = html.replace(INPUT_NUMBER, '$1riot-type='+brackets(0)+'"number"'+brackets(1)) // fake expression // alter special attribute names html = html.replace(SET_ATTR, function(full, name, _, expr) { if (expr.indexOf(brackets(0)) >= 0) { name = name.toLowerCase() if (PREFIX_ATTR.indexOf(name) >= 0) name = 'riot-' + name // IE8 looses boolean attr values: `checked={ expr }` --> `__checked={ expr }` else if (BOOL_ATTR.indexOf(name) >= 0) name = '__' + name } return name + '="' + expr + '"' }) // run expressions trough parser if (opts.expr) { html = html.replace(brackets(EXPR), function(_, expr) { var ret = compileJS(expr, opts, type).trim().replace(/\r?\n|\r/g, '').trim() if (ret.slice(-1) == ';') ret = ret.slice(0, -1) return brackets(0) + ret + brackets(1) }) } // -> html = html.replace(CLOSED_TAG, function(_, name, attr) { var tag = '<' + name + (attr ? ' ' + attr.trim() : '') + '>' // Do not self-close HTML5 void tags if (VOID_TAGS.indexOf(name.toLowerCase()) == -1) tag += '' return tag }) // escape single quotes html = html.replace(/'/g, "\\'") // \{ jotain \} --> \\{ jotain \\} html = html.replace(brackets(/\\{|\\}/g), '\\$&') // compact: no whitespace between tags if (opts.compact) html = html.replace(/> <') return html } function riotjs(js) { // strip comments js = js.replace(LINE_COMMENT, '').replace(JS_COMMENT, '') // ES6 method signatures var lines = js.split('\n'), es6Ident = '' lines.forEach(function(line, i) { var l = line.trim() // method start if (l[0] != '}' && l.indexOf('(') > 0 && l.indexOf('function') == -1) { var end = /[{}]/.exec(l.slice(-1)), m = end && /(\s+)([\w]+)\s*\(([\w,\s]*)\)\s*\{/.exec(line) if (m && !/^(if|while|switch|for|catch)$/.test(m[2])) { lines[i] = m[1] + 'this.' + m[2] + ' = function(' + m[3] + ') {' // foo() { } if (end[0] == '}') { lines[i] += ' ' + l.slice(m[0].length - 1, -1) + '}.bind(this)' } else { es6Ident = m[1] } } } // method end if (line.slice(0, es6Ident.length + 1) == es6Ident + '}') { lines[i] = es6Ident + '}.bind(this);' es6Ident = '' } }) return lines.join('\n') } function scopedCSS (tag, style, type) { // 1. Remove CSS comments // 2. Find selectors and separate them by conmma // 3. keep special selectors as is // 4. prepend tag and [riot-tag] return style.replace(CSS_COMMENT, '').replace(CSS_SELECTOR, function (m, p1, p2) { return p1 + ' ' + p2.split(/\s*,\s*/g).map(function(sel) { var s = sel.trim() var t = (/:scope/.test(s) ? '' : ' ') + s.replace(/:scope/, '') return s[0] == '@' || s == 'from' || s == 'to' || /%$/.test(s) ? s : tag + t + ', [riot-tag="' + tag + '"]' + t }).join(',') }).trim() } function compileJS(js, opts, type) { var parser = opts.parser || (type ? riot.parsers.js[type] : riotjs) if (!parser) throw new Error('Parser not found "' + type + '"') return parser(js, opts) } function compileTemplate(lang, html) { var parser = riot.parsers.html[lang] if (!parser) throw new Error('Template parser not found "' + lang + '"') return parser(html) } function compileCSS(style, tag, type) { if (type == 'scoped-css') style = scopedCSS(tag, style) else if (riot.parsers.css[type]) style = riot.parsers.css[type](tag, style) return style.replace(/\s+/g, ' ').replace(/\\/g, '\\\\').replace(/'/g, "\\'").trim() } function compile(src, opts) { opts = opts || {} if (opts.brackets) riot.settings.brackets = opts.brackets if (opts.template) src = compileTemplate(opts.template, src) src = src.replace(LINE_TAG, function(_, tagName, html) { return mktag(tagName, compileHTML(html, opts), '', '', '') }) return src.replace(CUSTOM_TAG, function(_, tagName, attrs, html, js) { html = html || '' attrs = compileHTML(attrs, '', '') // js wrapped inside